package com.csthink.auditlog.config.builder;

import com.csthink.auditlog.domain.Change;
import com.csthink.common.infrastructure.util.Jackson2Util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;

import java.util.*;

/**
 * @author <a href="mailto:security.2009@live.cn">Mars</a>
 * @since 2023-09-12
 */
public class ChangeBuilder {

    private String type;
    private String id;
    private String keyword;
    private Object oldVal;
    private Object newVal;
    private String[] fields;

    public ChangeBuilder() {
    }

    public ChangeBuilder(String type, String id, String keyword, Object oldVal, Object newVal, String[] fields) {
        this.type = type;
        this.id = id;
        this.keyword = keyword;
        this.oldVal = oldVal;
        this.newVal = newVal;
        this.fields = fields;
    }

    public void setType(String type) {
        this.type = type;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setKeyword(String keyword) {
        this.keyword = keyword;
    }

    public void setOldVal(Object oldVal) {
        this.oldVal = oldVal;
    }

    public void setNewVal(Object newVal) {
        this.newVal = newVal;
    }

    public void setFields(String[] fields) {
        this.fields = fields;
    }

    private boolean isBuild = false;
    private Change change = new Change();

    public boolean hasChange() {
        return StringUtils.isNotEmpty(this.id) ||
                StringUtils.isNotEmpty(this.keyword) ||
                null != oldVal || newVal != null;
    }

    /**
     * For now, it just can handle one layer properties. We can improve this.
     *
     * @return
     */
    public Change build() {
        if (isBuild) {
            return change;
        }
        // build properties.
        change.setId(this.id);
        change.setType(this.type);
        change.setKeyword(this.keyword);
        change.setRawOldValue(this.oldVal);
        change.setRawNewValue(this.newVal);
        change.setRawOldValueStr(Jackson2Util.tryToJson(this.oldVal));
        change.setRawNewValueStr(Jackson2Util.tryToJson(this.newVal));

        // For update, we just transfer changed key to server.
        // So, if isUpdate = true && newVal."some key" == null, even if
        // oldVal."some key" != null, we do not record.
        boolean isUpdate = this.oldVal != null && this.newVal != null;
        // If hasOldVal = false, we do not setOldValue, so just store a null in DB.
        boolean hasOldVal = this.oldVal != null;
        // If hasNewVal = false, we do not setNewValue, so just store a null in DB.
        boolean hasNewVal = this.newVal != null;
        if (this.oldVal == null && this.newVal == null) {
            return change;
        } else if (this.oldVal == null) {
            this.oldVal = new HashMap<String, Object>();
        } else if (this.newVal == null) {
            this.newVal = new HashMap<String, Object>();
        }
        List<Change.KeyValue> oldVals = new ArrayList<>();
        List<Change.KeyValue> newVals = new ArrayList<>();
        Map<String, Object> oldMap = Jackson2Util.toMap(this.oldVal);
        Map<String, Object> newMap = Jackson2Util.toMap(this.newVal);
        // When fields have value, we just record fields listed in.
        if (this.fields != null && this.fields.length > 0) {
            for (String field : this.fields) {
                oldVals.add(new Change.KeyValue(field, convertToSimpleValue(oldMap.get(field))));
                newVals.add(new Change.KeyValue(field, convertToSimpleValue(newMap.get(field))));
            }
        } else {
            // Else, we compare old & new Value, to find out changes.
            Set<String> keySets = new HashSet<>();
            keySets.addAll(oldMap.keySet());
            keySets.addAll(newMap.keySet());
            for (String key : keySets) {
                Object oldPropertyValue = oldMap.get(key);
                Object newPropertyValue = newMap.get(key);
                if (oldPropertyValue == null && newPropertyValue == null) {
                    continue;
                }
                if (oldPropertyValue != null && oldPropertyValue.equals(newPropertyValue)) {
                    continue;
                }
                // Caution: This is for update.
                // When we do update, normally we just transfer changed key to
                // Server, so when property value is null, ignore.
                if (isUpdate && newPropertyValue == null) {
                    continue;
                }
                // Prevent complex object(another bean, list, map etc) to serialize
                // to a json object.
                // For now, ES's index mappings does not support.
                oldVals.add(new Change.KeyValue(key, convertToSimpleValue(oldPropertyValue)));
                newVals.add(new Change.KeyValue(key, convertToSimpleValue(newPropertyValue)));
            }
        }
        if (hasOldVal)
//            change.setOldValue(oldVals);
            change.setOldValueStr(Jackson2Util.tryToJson(oldVals));
        if (hasNewVal)
//            change.setNewValue(newVals);
            change.setNewValueStr(Jackson2Util.tryToJson(newVals));
        return change;
    }

    private Object convertToSimpleValue(Object raw) {
        if (raw == null || BeanUtils.isSimpleValueType(raw.getClass()))
            return raw;
        return Jackson2Util.toJson(raw);
    }
}
